Skip to main content

๐Ÿš€ Auto-Deploy to DigitalOcean with GitHub Actions

Set up a CI/CD pipeline that automatically builds and deploys your site to DigitalOcean every time changes are merged to the master branch.


๐Ÿค” How It Worksโ€‹

๐Ÿญ Analogy: Think of GitHub Actions as a robot factory worker. Every time you merge code to master, the robot wakes up, builds the latest version of your site, ships it to your server, and verifies everything looks good โ€” all without you lifting a finger.

Developer merges PR to master
โ”‚
โ–ผ
GitHub Actions triggers automatically
โ”‚
โ”œโ”€โ”€ 1. Checkout code
โ”œโ”€โ”€ 2. Install dependencies (npm ci)
โ”œโ”€โ”€ 3. Build the site (npm run build)
โ”œโ”€โ”€ 4. SSH into DigitalOcean Droplet
โ”œโ”€โ”€ 5. Deploy new version to server
โ”œโ”€โ”€ 6. Reload Nginx
โ””โ”€โ”€ 7. Verify site is live โœ…
(Rollback if anything fails โ†ฉ๏ธ)

๐Ÿ”‘ Step 1 โ€” Generate a Dedicated Deploy SSH Keyโ€‹

Create a separate SSH key pair just for deployments (never reuse your personal key):

# Run this on your LOCAL machine
ssh-keygen -t ed25519 -C "github-deploy-key" -f ~/.ssh/github_deploy_key

# This creates two files:
# ~/.ssh/github_deploy_key โ† Private key (goes into GitHub Secrets)
# ~/.ssh/github_deploy_key.pub โ† Public key (goes onto your server)

๐Ÿ–ฅ๏ธ Step 2 โ€” Add the Public Key to Your Serverโ€‹

SSH into your DigitalOcean Droplet and authorize the deploy key:

ssh root@YOUR_DROPLET_IP

# On the server, add the public key to authorized_keys
echo "PASTE_YOUR_PUBLIC_KEY_HERE" >> ~/.ssh/authorized_keys

# Ensure correct permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Alternatively, from your local machine:

ssh-copy-id -i ~/.ssh/github_deploy_key.pub root@YOUR_DROPLET_IP

๐Ÿ” Step 3 โ€” Add GitHub Repository Secretsโ€‹

Your workflow needs three secrets to connect to DigitalOcean securely. Never hardcode these in your code.

  1. Go to your GitHub repo โ†’ Settings โ†’ Secrets and variables โ†’ Actions โ†’ New repository secret
  2. Add these three secrets:
Secret NameValueExample
DROPLET_SSH_KEYContent of your private key file-----BEGIN OPENSSH PRIVATE KEY-----\n...
DROPLET_IPYour Droplet's IP address152.42.157.67
DROPLET_USERSSH user on your Dropletroot

To get the private key content:

cat ~/.ssh/github_deploy_key
# Copy the entire output including the BEGIN/END lines

โš™๏ธ Step 4 โ€” The GitHub Actions Workflowโ€‹

This repository already includes the workflow at .github/workflows/deploy.yml. It runs automatically on every push to master or main.

What the workflow doesโ€‹

StepWhat Happens
CheckoutDownloads the latest code
Setup Node.jsInstalls Node 20 with npm cache
Install dependenciesRuns npm ci (clean install)
BuildRuns npm run build to generate static files
Setup SSHWrites the private key from secrets and adds the server to known_hosts
Create directoriesCreates versioned deploy folders on the server
Save previous versionRemembers the last deploy for rollback
Deploy filesUses rsync to efficiently copy only changed files
Set permissions & reloadFixes file ownership and reloads Nginx
VerifyChecks the site returns HTTP 200
Rollback (on failure)Reverts to the previous version if any step fails
CleanupRemoves the SSH key from the runner

Versioned deploys (zero-downtime)โ€‹

Each deployment creates a new versioned folder:

/applications/second-brain/
โ”œโ”€โ”€ versions/
โ”‚ โ”œโ”€โ”€ v_20260218_120000/ โ† previous version
โ”‚ โ””โ”€โ”€ v_20260220_093045/ โ† current version
โ”œโ”€โ”€ latest -> versions/v_20260220_093045 โ† symlink (what Nginx serves)
โ””โ”€โ”€ .backups/
โ”œโ”€โ”€ current_version
โ””โ”€โ”€ previous_version

Nginx points to /applications/second-brain/latest. Switching versions is instant โ€” just update the symlink.


๐Ÿ—๏ธ Step 5 โ€” Configure Nginx to Serve the Versioned Deployโ€‹

On your server, point Nginx's root directive at the latest symlink:

# /etc/nginx/sites-available/second-brain
server {
listen 80;
listen [::]:80;

server_name second-brain.dkbrainhub.com;

root /applications/second-brain/latest;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}

# ... (gzip, caching, security headers โ€” see Nginx guide)
}
# Enable and reload
ln -s /etc/nginx/sites-available/second-brain /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

โ–ถ๏ธ Step 6 โ€” Trigger Your First Deploymentโ€‹

You can trigger the workflow in two ways:

Merge any PR to master โ€” the workflow starts within seconds.

Manually (for testing)โ€‹

  1. Go to your repo on GitHub
  2. Click Actions tab
  3. Select Deploy to DigitalOcean
  4. Click Run workflow โ†’ Run workflow

๐Ÿ”„ How Rollback Worksโ€‹

If any step in the deployment fails, the Rollback on failure step automatically runs:

# What the rollback step does on the server:
PREVIOUS=$(cat /applications/second-brain/.backups/previous_version)
rm -f /applications/second-brain/latest
ln -s /applications/second-brain/versions/$PREVIOUS /applications/second-brain/latest
systemctl reload nginx

โ†ฉ๏ธ Analogy: It's like a time machine. If the new delivery is broken, the receptionist (Nginx) immediately starts directing guests back to the previous good version.


๐Ÿงช Verify the Pipeline Is Workingโ€‹

After your first successful deployment:

# SSH into server and check the deployed files
ssh root@YOUR_DROPLET_IP
ls /applications/second-brain/versions/
readlink /applications/second-brain/latest
cat /applications/second-brain/.backups/current_version

# Check the site
curl -I http://second-brain.dkbrainhub.com
# Expected: HTTP/1.1 200 OK

๐Ÿ“‹ Checklist: Full Setupโ€‹

  • SSH key pair generated (~/.ssh/github_deploy_key)
  • Public key added to server's ~/.ssh/authorized_keys
  • GitHub Secrets added: DROPLET_SSH_KEY, DROPLET_IP, DROPLET_USER
  • .github/workflows/deploy.yml exists in your repo
  • Nginx configured to serve /applications/second-brain/latest
  • DNS A record pointing second-brain.dkbrainhub.com โ†’ Droplet IP
  • First deployment triggered and verified โœ…
  • SSL certificate obtained (certbot --nginx -d second-brain.dkbrainhub.com)

๐Ÿ› Troubleshootingโ€‹

ProblemSolution
Permission denied (publickey)Check the private key was copied correctly into DROPLET_SSH_KEY secret. Include all lines including -----BEGIN / -----END.
Host key verification failedThe workflow uses ssh-keyscan automatically. If it fails, SSH in manually once from the runner to accept the fingerprint.
HTTP 502 Bad GatewayNginx is running but the app hasn't deployed yet, or the symlink is broken. Check readlink /applications/second-brain/latest.
rsync: connection unexpectedly closedSSH connection issue. Verify DROPLET_IP and DROPLET_USER secrets are correct.
Build fails locally but works in CIEnsure package-lock.json is committed. The workflow uses npm ci which requires it.

๐Ÿ“š Resourcesโ€‹

ResourceLink
GitHub Actions Docsdocs.github.com/actions
DigitalOcean SSH DocsHow to Set Up SSH Keys
GitHub Encrypted SecretsUsing secrets in GitHub Actions
rsync Manuallinux.die.net/man/1/rsync

Last updated: February 2026